package org.javaboy.auth_server.config;

import com.nimbusds.jose.jwk.JWKSet;
import com.nimbusds.jose.jwk.RSAKey;
import com.nimbusds.jose.jwk.source.ImmutableJWKSet;
import com.nimbusds.jose.jwk.source.JWKSource;
import com.nimbusds.jose.proc.SecurityContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.annotation.Order;
import org.springframework.core.io.ClassPathResource;
import org.springframework.jdbc.core.JdbcOperations;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.security.config.Customizer;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.oauth2.core.AuthorizationGrantType;
import org.springframework.security.oauth2.core.ClientAuthenticationMethod;
import org.springframework.security.oauth2.core.oidc.OidcScopes;
import org.springframework.security.oauth2.core.oidc.OidcUserInfo;
import org.springframework.security.oauth2.core.oidc.endpoint.OidcParameterNames;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.JdbcOAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationConsentService;
import org.springframework.security.oauth2.server.authorization.OAuth2AuthorizationService;
import org.springframework.security.oauth2.server.authorization.client.InMemoryRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.JdbcRegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClient;
import org.springframework.security.oauth2.server.authorization.client.RegisteredClientRepository;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configuration.OAuth2AuthorizationServerConfiguration;
import org.springframework.security.oauth2.server.authorization.config.annotation.web.configurers.OAuth2AuthorizationServerConfigurer;
import org.springframework.security.oauth2.server.authorization.settings.ClientSettings;
import org.springframework.security.oauth2.server.authorization.settings.OAuth2TokenFormat;
import org.springframework.security.oauth2.server.authorization.settings.TokenSettings;
import org.springframework.security.oauth2.server.authorization.token.JwtEncodingContext;
import org.springframework.security.oauth2.server.authorization.token.OAuth2TokenCustomizer;
import org.springframework.security.provisioning.InMemoryUserDetailsManager;
import org.springframework.security.web.SecurityFilterChain;
import org.springframework.security.web.authentication.LoginUrlAuthenticationEntryPoint;
import org.springframework.util.FileCopyUtils;

import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.security.KeyFactory;
import java.security.NoSuchAlgorithmException;
import java.security.PrivateKey;
import java.security.PublicKey;
import java.security.interfaces.RSAPublicKey;
import java.security.spec.InvalidKeySpecException;
import java.security.spec.PKCS8EncodedKeySpec;
import java.security.spec.X509EncodedKeySpec;
import java.time.Duration;
import java.time.Instant;
import java.util.Base64;
import java.util.UUID;

/**
 * 授权服务器的配置/认证服务器配置
 * <p>
 * 1. 客户端发送请求，从授权服务器获取授权码 code
 * 2. 授权服务器收到请求，发现客户端没登录，于是跳转到登录页面，让用户登录
 * 3. 用户登录成功后，授权服务器跳转回客户端，并携带授权码 code
 * 4. 客户端拿到授权码 code，请求授权服务器换取 token，有了 token，就可以访问资源服务器
 * 5. 客户端拿着 token 访问资源服务器，资源服务器验证 token（默认需要调用授权服务器验证），如果通过，就返回数据
 */
@Configuration
public class OAuth2ServerConfig {

    /**
     * 配置和 OAuth2 相关的过滤器链，即这个过滤器链只拦截和 OAuth2 相关的请求
     *
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    @Order(1)
    SecurityFilterChain oauth2ServerSecurityFilterChain(HttpSecurity http) throws Exception {
        //配置默认的拦截规则，包括关闭 CSRF 等
        OAuth2AuthorizationServerConfiguration.applyDefaultSecurity(http);
        //开启 OIDC 模式
        http.getConfigurer(OAuth2AuthorizationServerConfigurer.class).oidc(Customizer.withDefaults());
        //配置登录页
        http.exceptionHandling(e -> e.authenticationEntryPoint(new LoginUrlAuthenticationEntryPoint("/login.html")))
                //让授权服务器作为一个资源服务器，将来真正的资源服务器要来获取公钥信息，根据公钥去判断 token 是否合法
                .oauth2ResourceServer(rs -> rs.jwt(Customizer.withDefaults()));
        return http.build();
    }

    /**
     * OAuth2 之外的其他请求，在这个过滤器链中拦截
     *
     * @param http
     * @return
     * @throws Exception
     */
    @Bean
    @Order(2)
    SecurityFilterChain defaultFilterChain(HttpSecurity http) throws Exception {
        http.authorizeRequests(a -> a.anyRequest().authenticated())
                .formLogin(f -> f.loginPage("/login.html").loginProcessingUrl("/login").permitAll())
                .csrf(c -> c.disable());
        return http.build();
    }

    @Bean
    UserDetailsService us() {
        InMemoryUserDetailsManager manager = new InMemoryUserDetailsManager();
        manager.createUser(User.withUsername("javaboy").password("{noop}123").roles("USER").build());
        return manager;
    }

    @Bean
    RegisteredClientRepository registeredClientRepository2(JdbcTemplate jdbcTemplate) {
        return new JdbcRegisteredClientRepository(jdbcTemplate);
    }

    @Bean
    OAuth2AuthorizationService auth2AuthorizationService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationService(jdbcTemplate, registeredClientRepository);
    }

    @Bean
    OAuth2AuthorizationConsentService oAuth2AuthorizationConsentService(JdbcTemplate jdbcTemplate, RegisteredClientRepository registeredClientRepository) {
        return new JdbcOAuth2AuthorizationConsentService(jdbcTemplate, registeredClientRepository);
    }

    /**
     * 这个 Bean 返回的是客户端的信息
     * 因为我当前这个项目 auth_server 作为授权服务器，如果有客户端想要使用我的第三方登录功能，那么必须要先来我这里注册一下客户端的信息，注册完成之后，才能使用我的第三方登录功能
     *
     * @return
     */
//    @Bean
    RegisteredClientRepository registeredClientRepository(JdbcTemplate jdbcTemplate) {
        RegisteredClient client = RegisteredClient
                //客户端 ID，随机生成即可
                .withId(UUID.randomUUID().toString())
                //接下来的信息就是用户注册的时候自己填入进来的
                .clientId("client01")
                .clientSecret("{noop}client01_secret")
                //客户端密码过期时间
                .clientSecretExpiresAt(Instant.now().plusSeconds(60 * 60 * 24 * 365 * 3))
                //这个表示客户端传递信息的时候放在请求体中
                .clientAuthenticationMethod(ClientAuthenticationMethod.CLIENT_SECRET_POST)
                //这是算是 PKCE 模式专用的一个数据传输方式
//                .clientAuthenticationMethod(ClientAuthenticationMethod.NONE)
                //授权码模式
                .authorizationGrantType(AuthorizationGrantType.AUTHORIZATION_CODE)
                .authorizationGrantType(AuthorizationGrantType.REFRESH_TOKEN)
                //客户端模式
                .authorizationGrantType(AuthorizationGrantType.CLIENT_CREDENTIALS)
                //重定向的地址，授权成功之后，自动跳转到这个地址
                //什么是授权成功？授权服务器返回 code 给客户端，就是授权成功
                //注意这个地址是固定的，这个地址其实是 Spring Security 自动生成的，不需要我们额外处理
                //客户端的这个接口会从请求地址中获取到 code，然后根据 code 去 auth_server 获取 token
                .redirectUri("http://client.javaboy.org:8082/login/oauth2/code/client01")
                //授权的范围/也可以理解为是客户端的权限
                .scope("message:read")
                .scope("message:write")
                .scope(OidcScopes.OPENID)
                //配置生成的令牌的信息
                .tokenSettings(TokenSettings.builder()
                        //配置生成的令牌格式，令牌有两种格式：透明令牌和不透明令牌：
                        //透明令牌：令牌自带描述信息，令牌是有含义的，可以根据令牌提取出来一些想要的信息，例如 JWT
                        //不透明令牌：令牌没有描述信息，令牌就是令牌，没有任何含义，这种令牌一般就是一个 UUID 字符串
                        //透明令牌，资源服务器可以直接解析
                        //不透明令牌必须来到授权服务器校验
                        .accessTokenFormat(OAuth2TokenFormat.SELF_CONTAINED)
                        .accessTokenTimeToLive(Duration.ofDays(7))
                        .refreshTokenTimeToLive(Duration.ofDays(3))
                        .build())
                //客户端的配置
                .clientSettings(ClientSettings.builder()
                        //开启 PKCE 模式
//                                .requireProofKey(true)
                        //这个是弹出授权页面，让用户自己选择权限
                        .requireAuthorizationConsent(true).build()).build();
        JdbcRegisteredClientRepository repository = new JdbcRegisteredClientRepository(jdbcTemplate);
        //如果数据库中没有这个客户端，那么就保存到数据库中
        if (repository.findByClientId("client01") == null) {
            repository.save(client);
        }
        return repository;
    }

    @Bean
    JWKSource<SecurityContext> jwkSource() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        RSAKey rsaKey = new RSAKey.Builder(getPublicKey())
                .privateKey(getPrivateKey())
                .keyID(UUID.randomUUID().toString())
                .build();
        JWKSet jwkSet = new JWKSet(rsaKey);
        return new ImmutableJWKSet<>(jwkSet);
    }

    public PrivateKey getPrivateKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        ClassPathResource resource = new ClassPathResource("private_key.pem");
        byte[] privateKeyBytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
        String key = new String(privateKeyBytes, StandardCharsets.UTF_8);
        String privateStr = key
                .replace("-----BEGIN PRIVATE KEY-----", "")
                .replaceAll(System.lineSeparator(), "")
                .replace("-----END PRIVATE KEY-----", "");
        byte[] privateKeyDecodedBytes = Base64.getDecoder().decode(privateStr);
        PKCS8EncodedKeySpec keySpec = new PKCS8EncodedKeySpec(privateKeyDecodedBytes);
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        PrivateKey privateKey = keyFactory.generatePrivate(keySpec);
        return privateKey;
    }

    public RSAPublicKey getPublicKey() throws IOException, NoSuchAlgorithmException, InvalidKeySpecException {
        ClassPathResource resource = new ClassPathResource("public_key.pem");
        byte[] publicKeyBytes = FileCopyUtils.copyToByteArray(resource.getInputStream());
        String key = new String(publicKeyBytes, StandardCharsets.UTF_8);
        String publicStr = key
                .replace("-----BEGIN PUBLIC KEY-----", "")
                .replaceAll(System.lineSeparator(), "")
                .replace("-----END PUBLIC KEY-----", "");
        KeyFactory keyFactory = KeyFactory.getInstance("RSA");
        byte[] publicKeyBase64Bytes = Base64.getDecoder().decode(publicStr);
        X509EncodedKeySpec publicKeySpec = new X509EncodedKeySpec(publicKeyBase64Bytes);
        PublicKey rsaPublicKey = keyFactory.generatePublic(publicKeySpec);
        return (RSAPublicKey) rsaPublicKey;
    }

    @Bean
    OAuth2TokenCustomizer<JwtEncodingContext> tokenCustomizer() {
        return context -> {
            if (context.getTokenType().getValue().equals(OidcParameterNames.ID_TOKEN)) {
                OidcUserInfo userInfo = OidcUserInfo.builder()
                        .name("javaboy")
                        .email("111@qq.com")
                        .birthdate("2000-01-01")
                        .gender("男")
                        .build();
                context.getClaims().claims(claims -> claims.putAll(userInfo.getClaims()));
            }
        };
    }
}
